import UIKit
@available(iOSApplicationExtension, unavailable)
public extension IQKeyboardManager {
    private struct AssociatedKeys {
        static var movedDistance: Int = 0
        static var movedDistanceChanged: Int = 0
        static var lastScrollView: Int = 0
        static var startingContentOffset: Int = 0
        static var startingScrollIndicatorInsets: Int = 0
        static var startingContentInsets: Int = 0
        static var startingTextViewContentInsets: Int = 0
        static var startingTextViewScrollIndicatorInsets: Int = 0
        static var isTextViewContentInsetChanged: Int = 0
    }
    @objc private(set) var movedDistance: CGFloat {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.movedDistance) as? CGFloat ?? 0.0
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.movedDistance, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            movedDistanceChanged?(movedDistance)
        }
    }
    @objc var movedDistanceChanged: ((CGFloat) -> Void)? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.movedDistanceChanged) as? ((CGFloat) -> Void)
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.movedDistanceChanged, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            movedDistanceChanged?(movedDistance)
        }
    }
    internal weak var lastScrollView: UIScrollView? {
        get {
            return (objc_getAssociatedObject(self, &AssociatedKeys.lastScrollView) as? WeakObjectContainer)?.object as? UIScrollView
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.lastScrollView, WeakObjectContainer(object: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var startingContentOffset: CGPoint {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.startingContentOffset) as? CGPoint ?? IQKeyboardManager.kIQCGPointInvalid
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.startingContentOffset, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var startingScrollIndicatorInsets: UIEdgeInsets {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.startingScrollIndicatorInsets) as? UIEdgeInsets ?? .init()
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.startingScrollIndicatorInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var startingContentInsets: UIEdgeInsets {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.startingContentInsets) as? UIEdgeInsets ?? .init()
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.startingContentInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var startingTextViewContentInsets: UIEdgeInsets {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.startingTextViewContentInsets) as? UIEdgeInsets ?? .init()
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.startingTextViewContentInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var startingTextViewScrollIndicatorInsets: UIEdgeInsets {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.startingTextViewScrollIndicatorInsets) as? UIEdgeInsets ?? .init()
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.startingTextViewScrollIndicatorInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var isTextViewContentInsetChanged: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.isTextViewContentInsetChanged) as? Bool ?? false
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.isTextViewContentInsetChanged, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    @objc internal func applicationDidBecomeActive(_ notificatin: Notification) {
        guard privateIsEnabled(),
            keyboardShowing,
            topViewBeginOrigin.equalTo(IQKeyboardManager.kIQCGPointInvalid) == false, let textFieldView = textFieldView,
            textFieldView.isAlertViewTextField() == false else {
                return
        }
        self.adjustPosition()
     }
    internal func adjustPosition() {
        guard UIApplication.shared.applicationState == .active,
              let textFieldView = textFieldView,
            let rootController = textFieldView.parentContainerViewController(),
            let window = keyWindow(),
            let textFieldViewRectInWindow = textFieldView.superview?.convert(textFieldView.frame, to: window),
            let textFieldViewRectInRootSuperview = textFieldView.superview?.convert(textFieldView.frame, to: rootController.view?.superview) else {
                return
        }
        let startTime = CACurrentMediaTime()
        showLog(">>>>> \(#function) started >>>>>", indentation: 1)
        var rootViewOrigin = rootController.view.frame.origin
        let specialKeyboardDistanceFromTextField: CGFloat
        if let searchBar = textFieldView.textFieldSearchBar() {
            specialKeyboardDistanceFromTextField = searchBar.keyboardDistanceFromTextField
        } else {
            specialKeyboardDistanceFromTextField = textFieldView.keyboardDistanceFromTextField
        }
        let newKeyboardDistanceFromTextField = (specialKeyboardDistanceFromTextField == kIQUseDefaultKeyboardDistance) ? keyboardDistanceFromTextField : specialKeyboardDistanceFromTextField
        let kbSize: CGSize
        let originalKbSize: CGSize
        do {
            var kbFrame = keyboardFrame
            kbFrame.origin.y -= newKeyboardDistanceFromTextField
            kbFrame.size.height += newKeyboardDistanceFromTextField
            kbFrame.origin.y -= topViewBeginSafeAreaInsets.bottom
            kbFrame.size.height += topViewBeginSafeAreaInsets.bottom
            let intersectRect = kbFrame.intersection(window.frame)
            if intersectRect.isNull {
                kbSize = CGSize(width: kbFrame.size.width, height: 0)
            } else {
                kbSize = intersectRect.size
            }
        }
        do {
            let intersectRect = keyboardFrame.intersection(window.frame)
            if intersectRect.isNull {
                originalKbSize = CGSize(width: keyboardFrame.size.width, height: 0)
            } else {
                originalKbSize = intersectRect.size
            }
        }
        let statusBarHeight: CGFloat
        let navigationBarAreaHeight: CGFloat
        if let navigationController = rootController.navigationController {
            navigationBarAreaHeight = navigationController.navigationBar.frame.maxY
        } else {
#if swift(>=5.1)
            if #available(iOS 13, *) {
                statusBarHeight = window.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
            } else {
                statusBarHeight = UIApplication.shared.statusBarFrame.height
            }
#else
            statusBarHeight = UIApplication.shared.statusBarFrame.height
#endif
            navigationBarAreaHeight = statusBarHeight
        }
        let layoutAreaHeight: CGFloat = rootController.view.directionalLayoutMargins.top
        let isScrollableTextView: Bool
        if let textView = textFieldView as? UIScrollView, textFieldView.responds(to: #selector(getter: UITextView.isEditable)) {
            isScrollableTextView = textView.isScrollEnabled
        } else {
            isScrollableTextView = false
        }
        let topLayoutGuide: CGFloat = max(navigationBarAreaHeight, layoutAreaHeight)
        let bottomLayoutGuide: CGFloat = isScrollableTextView ? 0 : rootController.view.directionalLayoutMargins.bottom
        var moveUp: CGFloat
        do {
            let visibleHeight: CGFloat = window.frame.height-kbSize.height
            let topMovement: CGFloat = textFieldViewRectInRootSuperview.minY-topLayoutGuide
            let bottomMovement: CGFloat = textFieldViewRectInWindow.maxY - visibleHeight + bottomLayoutGuide
            moveUp = min(topMovement, bottomMovement)
        }
        showLog("Need to move: \(moveUp), will be moving \(moveUp < 0 ? "down" : "up")")
        var superScrollView: UIScrollView?
        var superView = textFieldView.superviewOfClassType(UIScrollView.self) as? UIScrollView
        while let view = superView {
            if view.isScrollEnabled, !view.shouldIgnoreScrollingAdjustment {
                superScrollView = view
                break
            } else {
                superView = view.superviewOfClassType(UIScrollView.self) as? UIScrollView
            }
        }
        if let lastScrollView = lastScrollView {
            if superScrollView == nil {
                if lastScrollView.contentInset != self.startingContentInsets {
                    showLog("Restoring contentInset to: \(startingContentInsets)")
                    UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                        lastScrollView.contentInset = self.startingContentInsets
                        lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
                    })
                }
                if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(startingContentOffset) {
                    showLog("Restoring contentOffset to: \(startingContentOffset)")
                    let animatedContentOffset = textFieldView.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil 
                    if animatedContentOffset {
                        lastScrollView.setContentOffset(startingContentOffset, animated: UIView.areAnimationsEnabled)
                    } else {
                        lastScrollView.contentOffset = startingContentOffset
                    }
                }
                startingContentInsets = .zero
                startingScrollIndicatorInsets = .zero
                startingContentOffset = .zero
                self.lastScrollView = nil
            } else if superScrollView != lastScrollView {    
                if lastScrollView.contentInset != self.startingContentInsets {
                    showLog("Restoring contentInset to: \(startingContentInsets)")
                    UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                        lastScrollView.contentInset = self.startingContentInsets
                        lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
                    })
                }
                if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(startingContentOffset) {
                    showLog("Restoring contentOffset to: \(startingContentOffset)")
                    let animatedContentOffset = textFieldView.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil 
                    if animatedContentOffset {
                        lastScrollView.setContentOffset(startingContentOffset, animated: UIView.areAnimationsEnabled)
                    } else {
                        lastScrollView.contentOffset = startingContentOffset
                    }
                }
                self.lastScrollView = superScrollView
                if let scrollView = superScrollView {
                    startingContentInsets = scrollView.contentInset
                    startingContentOffset = scrollView.contentOffset
                    #if swift(>=5.1)
                    if #available(iOS 11.1, *) {
                        startingScrollIndicatorInsets = scrollView.verticalScrollIndicatorInsets
                    } else {
                        startingScrollIndicatorInsets = scrollView.scrollIndicatorInsets
                    }
                    #else
                    startingScrollIndicatorInsets = scrollView.scrollIndicatorInsets
                    #endif
                }
                showLog("Saving ScrollView New contentInset: \(startingContentInsets) and contentOffset: \(startingContentOffset)")
            }
        } else if let unwrappedSuperScrollView = superScrollView {   
            lastScrollView = unwrappedSuperScrollView
            startingContentInsets = unwrappedSuperScrollView.contentInset
            startingContentOffset = unwrappedSuperScrollView.contentOffset
            #if swift(>=5.1)
            if #available(iOS 11.1, *) {
                startingScrollIndicatorInsets = unwrappedSuperScrollView.verticalScrollIndicatorInsets
            } else {
                startingScrollIndicatorInsets = unwrappedSuperScrollView.scrollIndicatorInsets
            }
            #else
            startingScrollIndicatorInsets = unwrappedSuperScrollView.scrollIndicatorInsets
            #endif
            showLog("Saving ScrollView contentInset: \(startingContentInsets) and contentOffset: \(startingContentOffset)")
        }
        if let lastScrollView = lastScrollView {
            var lastView = textFieldView
            var superScrollView = self.lastScrollView
            while let scrollView = superScrollView {
                var shouldContinue = false
                if moveUp > 0 {
                    shouldContinue =  moveUp > (-scrollView.contentOffset.y - scrollView.contentInset.top)
                } else if let tableView = scrollView.superviewOfClassType(UITableView.self) as? UITableView {
                    shouldContinue = scrollView.contentOffset.y > 0
                    if shouldContinue, let tableCell = textFieldView.superviewOfClassType(UITableViewCell.self) as? UITableViewCell, let indexPath = tableView.indexPath(for: tableCell), let previousIndexPath = tableView.previousIndexPath(of: indexPath) {
                        let previousCellRect = tableView.rectForRow(at: previousIndexPath)
                        if !previousCellRect.isEmpty {
                            let previousCellRectInRootSuperview = tableView.convert(previousCellRect, to: rootController.view.superview)
                            moveUp = min(0, previousCellRectInRootSuperview.maxY - topLayoutGuide)
                        }
                    }
                } else if let collectionView = scrollView.superviewOfClassType(UICollectionView.self) as? UICollectionView {
                    shouldContinue = scrollView.contentOffset.y > 0
                    if shouldContinue, let collectionCell = textFieldView.superviewOfClassType(UICollectionViewCell.self) as? UICollectionViewCell, let indexPath = collectionView.indexPath(for: collectionCell), let previousIndexPath = collectionView.previousIndexPath(of: indexPath), let attributes = collectionView.layoutAttributesForItem(at: previousIndexPath) {
                        let previousCellRect = attributes.frame
                        if !previousCellRect.isEmpty {
                            let previousCellRectInRootSuperview = collectionView.convert(previousCellRect, to: rootController.view.superview)
                            moveUp = min(0, previousCellRectInRootSuperview.maxY - topLayoutGuide)
                        }
                    }
                } else {
                    shouldContinue = textFieldViewRectInRootSuperview.minY < topLayoutGuide
                    if shouldContinue {
                        moveUp = min(0, textFieldViewRectInRootSuperview.minY - topLayoutGuide)
                    }
                }
                if shouldContinue {
                    var tempScrollView = scrollView.superviewOfClassType(UIScrollView.self) as? UIScrollView
                    var nextScrollView: UIScrollView?
                    while let view = tempScrollView {
                        if view.isScrollEnabled, !view.shouldIgnoreScrollingAdjustment {
                            nextScrollView = view
                            break
                        } else {
                            tempScrollView = view.superviewOfClassType(UIScrollView.self) as? UIScrollView
                        }
                    }
                    if let lastViewRect = lastView.superview?.convert(lastView.frame, to: scrollView) {
                        var shouldOffsetY = scrollView.contentOffset.y - min(scrollView.contentOffset.y, -moveUp)
                        shouldOffsetY = min(shouldOffsetY, lastViewRect.minY)
                        if isScrollableTextView,
                            nextScrollView == nil,
                            shouldOffsetY >= 0 {
                            if let currentTextFieldViewRect = textFieldView.superview?.convert(textFieldView.frame, to: window) {
                                let expectedFixDistance: CGFloat = currentTextFieldViewRect.minY - topLayoutGuide
                                shouldOffsetY = min(shouldOffsetY, scrollView.contentOffset.y + expectedFixDistance)
                                moveUp = 0
                            } else {
                                moveUp -= (shouldOffsetY-scrollView.contentOffset.y)
                            }
                        } else {
                            moveUp -= (shouldOffsetY-scrollView.contentOffset.y)
                        }
                        let newContentOffset = CGPoint(x: scrollView.contentOffset.x, y: shouldOffsetY)
                        if scrollView.contentOffset.equalTo(newContentOffset) == false {
                            showLog("old contentOffset: \(scrollView.contentOffset) new contentOffset: \(newContentOffset)")
                            self.showLog("Remaining Move: \(moveUp)")
                            UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                                let animatedContentOffset = textFieldView.superviewOfClassType(UIStackView.self, belowView: scrollView) != nil 
                                if animatedContentOffset {
                                    scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
                                } else {
                                    scrollView.contentOffset = newContentOffset
                                }
                            }, completion: { _ in
                                if scrollView is UITableView || scrollView is UICollectionView {
                                    self.addToolbarIfRequired()
                                }
                            })
                        }
                    }
                    lastView = scrollView
                    superScrollView = nextScrollView
                } else {
                    moveUp = 0
                    break
                }
            }
            if let lastScrollViewRect = lastScrollView.superview?.convert(lastScrollView.frame, to: window),
                lastScrollView.shouldIgnoreContentInsetAdjustment == false {
                var bottomInset: CGFloat = (kbSize.height)-(window.frame.height-lastScrollViewRect.maxY)
                var bottomScrollIndicatorInset = bottomInset - newKeyboardDistanceFromTextField - topViewBeginSafeAreaInsets.bottom
                bottomInset = max(startingContentInsets.bottom, bottomInset)
                bottomScrollIndicatorInset = max(startingScrollIndicatorInsets.bottom, bottomScrollIndicatorInset)
                bottomInset -= lastScrollView.safeAreaInsets.bottom
                bottomScrollIndicatorInset -= lastScrollView.safeAreaInsets.bottom
                var movedInsets = lastScrollView.contentInset
                movedInsets.bottom = bottomInset
                if lastScrollView.contentInset != movedInsets {
                    showLog("old ContentInset: \(lastScrollView.contentInset) new ContentInset: \(movedInsets)")
                    UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                        lastScrollView.contentInset = movedInsets
                        var newScrollIndicatorInset: UIEdgeInsets
                        #if swift(>=5.1)
                        if #available(iOS 11.1, *) {
                            newScrollIndicatorInset = lastScrollView.verticalScrollIndicatorInsets
                        } else {
                            newScrollIndicatorInset = lastScrollView.scrollIndicatorInsets
                        }
                        #else
                        newScrollIndicatorInset = lastScrollView.scrollIndicatorInsets
                        #endif
                        newScrollIndicatorInset.bottom = bottomScrollIndicatorInset
                        lastScrollView.scrollIndicatorInsets = newScrollIndicatorInset
                    })
                }
            }
        }
        if isScrollableTextView, let textView = textFieldView as? UIScrollView {
            let keyboardYPosition = window.frame.height - originalKbSize.height
            var rootSuperViewFrameInWindow = window.frame
            if let rootSuperview = rootController.view.superview {
                rootSuperViewFrameInWindow = rootSuperview.convert(rootSuperview.bounds, to: window)
            }
            let keyboardOverlapping = rootSuperViewFrameInWindow.maxY - keyboardYPosition
            let textViewHeight = min(textView.frame.height, rootSuperViewFrameInWindow.height-topLayoutGuide-keyboardOverlapping)
            if textView.frame.size.height-textView.contentInset.bottom>textViewHeight {
                if !self.isTextViewContentInsetChanged {
                    self.startingTextViewContentInsets = textView.contentInset
                    #if swift(>=5.1)
                    if #available(iOS 11.1, *) {
                        self.startingTextViewScrollIndicatorInsets = textView.verticalScrollIndicatorInsets
                    } else {
                        self.startingTextViewScrollIndicatorInsets = textView.scrollIndicatorInsets
                    }
                    #else
                    self.startingTextViewScrollIndicatorInsets = textView.scrollIndicatorInsets
                    #endif
                }
                self.isTextViewContentInsetChanged = true
                var newContentInset = textView.contentInset
                newContentInset.bottom = textView.frame.size.height-textViewHeight
                newContentInset.bottom -= textView.safeAreaInsets.bottom
                if textView.contentInset != newContentInset {
                    self.showLog("\(textFieldView) Old UITextView.contentInset: \(textView.contentInset) New UITextView.contentInset: \(newContentInset)")
                    UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                        textView.contentInset = newContentInset
                        textView.scrollIndicatorInsets = newContentInset
                    }, completion: { (_) -> Void in })
                }
            }
        }
        if moveUp >= 0 {
            rootViewOrigin.y = max(rootViewOrigin.y - moveUp, min(0, -originalKbSize.height))
            if rootController.view.frame.origin.equalTo(rootViewOrigin) == false {
                showLog("Moving Upward")
                UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                    var rect = rootController.view.frame
                    rect.origin = rootViewOrigin
                    rootController.view.frame = rect
                    if self.layoutIfNeededOnUpdate {
                        rootController.view.setNeedsLayout()
                        rootController.view.layoutIfNeeded()
                    }
                    self.showLog("Set \(rootController) origin to: \(rootViewOrigin)")
                })
            }
            movedDistance = (topViewBeginOrigin.y-rootViewOrigin.y)
        } else { 
            let disturbDistance: CGFloat = rootViewOrigin.y-topViewBeginOrigin.y
            if disturbDistance <= 0 {
                rootViewOrigin.y -= max(moveUp, disturbDistance)
                if rootController.view.frame.origin.equalTo(rootViewOrigin) == false {
                    showLog("Moving Downward")
                    UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                        var rect = rootController.view.frame
                        rect.origin = rootViewOrigin
                        rootController.view.frame = rect
                        if self.layoutIfNeededOnUpdate {
                            rootController.view.setNeedsLayout()
                            rootController.view.layoutIfNeeded()
                        }
                        self.showLog("Set \(rootController) origin to: \(rootViewOrigin)")
                    })
                }
                movedDistance = (topViewBeginOrigin.y-rootViewOrigin.y)
            }
        }
        let elapsedTime = CACurrentMediaTime() - startTime
        showLog("<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
    }
    internal func restorePosition() {
        guard topViewBeginOrigin.equalTo(IQKeyboardManager.kIQCGPointInvalid) == false, let rootViewController = rootViewController else {
            return
        }
        if rootViewController.view.frame.origin.equalTo(self.topViewBeginOrigin) == false {
            UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                var rect = rootViewController.view.frame
                rect.origin = self.topViewBeginOrigin
                rootViewController.view.frame = rect
                if self.layoutIfNeededOnUpdate {
                    rootViewController.view.setNeedsLayout()
                    rootViewController.view.layoutIfNeeded()
                }
                self.showLog("Restoring \(rootViewController) origin to: \(self.topViewBeginOrigin)")
            })
        }
        self.movedDistance = 0
        if rootViewController.navigationController?.interactivePopGestureRecognizer?.state == .began {
            self.rootViewControllerWhilePopGestureRecognizerActive = rootViewController
            self.topViewBeginOriginWhilePopGestureRecognizerActive = self.topViewBeginOrigin
        }
        self.rootViewController = nil
    }
}
